import java.util.*;

import entries.*;

import org.antlr.runtime.tree.CommonTree;

/**
 * SymbolTable class
 * The SymbolTable keeps track of all identifiers and their values
 * The SymbolTable also keeps track of the scope an identifier is 
 * declared in, to be able to apply scope rules
 * @author Keith Davelaar
 * @version 2012.06.04
 *
 */
public class SymbolTable {
	
	/** Map which contains all Entries*/
	private Map<String, Entry> entries;
	/** List which contains all parameters */
	private Map<String, Character> parameters;
	/** Current scope level */
	private int level;
	
	/**
	 * Constructor
	 * Create a new SymbolTable with a new Entry table and scope level 0
	 */
	public SymbolTable(){
		entries = new HashMap<String, Entry>();
		parameters = new HashMap<String, Character>();
		level = 0;
	}
	
	/**
	 * Get the type of an identifier or parameter
	 * @param id The name of the identifier
	 * @return the type of identifier id
	 */
	public char getType(String id){ 
		if(parameters.containsKey(id)){
			return parameters.get(id);
		}
		else{
			return entries.get(id).getType();
		}
		 
	}
	
	/** Open a new scope */
	public void openScope(){ level++; }
	
	/** Close the current scope after removing all entries of this scope */
	public void closeScope(){	
		Set<String> results = new HashSet<String>();
		for (String id : entries.keySet()) {
			Entry entry = entries.get(id);
			if(entry.getKind() == Entry.CONSTANT){
				if (((Constant) entries.get(id)).getLevel() == level) {
					results.add(id);
				}
			}else if(entry.getKind() == Entry.VARIABLE){
				if (((Variable) entries.get(id)).getLevel() == level) {
					results.add(id);
				}
			}
		}
		for (String id : results) {
			entries.remove(id);
		}
		level--;
	}
	
	/**
	 * Check if an identifier has been declared
	 * @param tree The tree of the declaration, used for error propagation
	 * @param id The name of the identifier
	 * @throws CompilerException
	 */
	public void isDeclared(CommonTree tree, String id) throws CompilerException{
		if(!entries.containsKey(id) && !parameters.containsKey(id)){
			throw new CompilerException(tree, id+" isn't declared yet.");
		}
	}	
	
	/**
	 * Check if an identifier has a value assigned to it, otherwise it can't be used
	 * @param tree The tree where the check occurs, used for error propagation
	 * @param id The name of the identifier
	 * @throws CompilerException
	 */
	public void isAssigned(CommonTree tree, String id) throws CompilerException{
		Entry entry = entries.get(id);
		if (!parameters.containsKey(id)) {
			if (entry == null) {
				throw new CompilerException(tree, id + " isn't declared yet.");
			}
			if (entry.getKind() == Entry.VARIABLE) {
				if (!((Variable) entry).isAssigned()) {
					throw new CompilerException(tree,
							"Trying to perform an operation on variable " + id
									+ " which hasn't had a value assigned yet");
				}
			}
		}
	}
	
	/**
	 * Check the declaration of a constant
	 * @param tree The tree where the constant is being created, used for error propagation
	 * @param id The name of the constant
	 * @param type The type of the constant
	 * @throws CompilerException
	 */
	public void constantDeclaration(CommonTree tree, String id, char type) throws CompilerException{
		if(entries.containsKey(id) || parameters.containsKey(id)){
			throw new CompilerException(tree, id+" is already declared and cannot be used again.");
		}else{
			entries.put(id, new Constant(type, level));
		}
	}
	
	/**
	 * Check the declaration of a variable
	 * @param tree The tree where the variable is being created, used for error propagation
	 * @param id The name of the variable
	 * @param type The type of the variable
	 * @throws CompilerException
	 */
	public void variableDeclaration(CommonTree tree, String id, String type) throws CompilerException{
		if(entries.containsKey(id) || parameters.containsKey(id)){
			throw new CompilerException(tree, id+" is already declared and cannot be used again.");
		}else{
			entries.put(id, new Variable(type, level));
		}
	}
	
	/**
	 * A value has been assigned to a variable, modify this in the SymbolTable
	 * @param tree The three of the modification, used for error propagation
	 * @param id The name of the variable, which has been assigned a value
	 * @throws CompilerException 
	 */
	public void assign(CommonTree tree, String id) throws CompilerException { 
		if(entries.containsKey(id)){
			Entry entry = entries.get(id);
			if(entry.getKind() == Entry.CONSTANT){
				throw new CompilerException(tree, "Trying to edit a constant, which isn't allowed.");
			}else if(entry.getKind() == Entry.VARIABLE){
				((Variable) entry).assign();
			}
		}else if(parameters.containsKey(id)){
			//Do nothing
		}else{
			throw new CompilerException(tree, id + " is not declared");
		}		
	}
	
	/**
	 * Create a new Procedure
	 * @param tree The tree where the procedure is being created, used for error propagation
	 * @param id The name of the Entry that is being created
	 * @throws CompilerException
	 */
	public void procedureCreation(CommonTree tree, String id) throws CompilerException {
		if(entries.containsKey(id)){
			throw new CompilerException(tree, id+" is already declared and cannot be used again.");
		}else{
			entries.put(id, new Procedure());
		}
	}
	
	/**
	 * Create a new Function
	 * @param tree The tree where the function is being created, used for error propagation
	 * @param id The name of the Entry that is being created
	 * @throws CompilerException
	 */
	public void functionCreation(CommonTree tree, String id) throws CompilerException {
		if(entries.containsKey(id)){
			throw new CompilerException(tree, id+" is already declared and cannot be used again.");
		}else{
			entries.put(id, new Function());
		}
	}

	/**
	 * Create a new parameter, if one already exists, then update the procedure/functions names in which it is used
	 * @param tree The tree where the parameter is being created, used for error propagation
	 * @param proc_func
	 * @param id
	 * @param type
	 * @param number
	 */
	public void parameterCreation(CommonTree tree, String proc_func, String id, String type, int number) {
		Entry entry = entries.get(proc_func);
		if(entry.getKind() == Entry.PROCEDURE){
			((Procedure) entry).addParameter(id, type, number);
			parameters.put(id, ((Procedure) entry).getParameterType(id));
		}
		if(entry.getKind() == Entry.FUNCTION){
			((Function) entry).addParameter(id, type, number);
			parameters.put(id, ((Function) entry).getParameterType(id));
		}
	}	

	/**
	 * Checks whether the typing for the Function statement is correct
	 * @param fc The Function statement tree, used for error propagation
	 * @param id The name of the function
	 * @param  returnType The type of the expression which is being returned
	 * @throws CompilerException 
	 */
	public void checkFunction(CodeChecker.f_compound_return fc, String id, String returnType) throws CompilerException {		
		Function function = (Function) entries.get(id);
		function.setType(returnType);	
		if(fc.retType != function.getType()){
			throw new CompilerException(fc.tree, "Return type "+function.getType()+" isn't equal to function type "+fc.retType);
		}
	}
	
	/**
	 * Check a function/procedure call
	 * @param tree The tree where there is a function/procedure call, used for error propagation
	 * @param id The name of the procedure/function which has been called
	 * @param pars The amount of parameters this procedure/function should have
	 * @throws CompilerException 
	 */
	public char checkCall(CommonTree tree, String id) throws CompilerException {
		if(!entries.containsKey(id)){
			throw new CompilerException(tree, "Procedure/Fucntion call to an unexisting procedure "+id);
		}
		return entries.get(id).getType();
	}

	/**
	 * Check the type of the filled in parameters, compared to what the function/procedure expects.
	 * @param pc The tree where the parameter is checked, used for error propagation
	 * @param id The id of the function/procedure being called
	 * @param types A list with all argument types
	 * @throws CompilerException 
	 */
	public void checkParameter(CommonTree pc, String id, List<Character> types) throws CompilerException {
		Entry entry = entries.get(id);	
		if(entry.getKind() == Entry.PROCEDURE){
			Procedure proc = (Procedure) entry;
			if(types.size() != (proc.getParamaters()+1)){
				throw new CompilerException(pc, "The amount of arguments isn't equal to the amount of arguments required for "+id);
			}
			for(int i = 0; i < types.size(); i++){
				if(proc.getParameterType(i) != types.get(i)){
					throw new CompilerException(pc, "Required type ("+proc.getParameterType(i)+") isn't what was entered ("+types.get(i)+")");
				}
			}
		}
		if(entry.getKind() == Entry.FUNCTION){
			Function proc = (Function) entry;
			if(types.size() != (proc.getParamaters()+1)){
				throw new CompilerException(pc, "The amount of arguments isn't equal to the amount of arguments required for "+id);
			}
			for(int i = 0; i < types.size(); i++){
				if(proc.getParameterType(i) != types.get(i)){
					throw new CompilerException(pc, "Required type ("+proc.getParameterType(i)+") isn't what was entered ("+types.get(i)+")");
				}
			}
		}
		
	}
}